﻿using Bettzueche.RLLibrary.Types;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;

namespace Bettzueche.Dampfross
{
    /// <summary>
    /// In diesem Experiment soll festgestellt bzw. bewiesen werden, dass sich für gleiche Zustände in verschiedenen Welten
    /// ein Mittelwert für die Q-Schätzwerte einstellt.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Das Feld <c>_StartKoordinaten2</c> hält für beide Umwelten eine jeweilige Auswahl von Startkoordinaten. Die Minimalkosten zur fixen
    /// Zielkoordinate betragen jeweils 12. Während des Experiments wird bei jedem Epsidonlauf die Umwelt gewechselt und eine 
    /// zufällige Startkoordinate aus der entsprechenden Auswahl gewählt.
    /// </para><para>
    /// Die Initialisierungsmethode <c>LadeTestUmgebungen(Karten,Karten)</c> lädt die spezifizierten Karten und Q-Sollwerte (tatsächliche Minimalkosten). Der vorhandene bzw.
    /// von der Methode neu erzeugte Agent wird in die erste der beiden Welten gesetzt. Eine ggf. aktuelle Lernepisode wird dabei automatisch beendet.
    /// Derzeit werden nur die beiden Welten Karten.Map9x10_1 und Karten.Map9x10_2 unterstützt.
    /// </para><para>
    /// Mit <c>StarteExperiment</c> wird der Test durchgeführt, wobei der Agent nach jeder Episode abwechselnd in die erste oder zweite Welt gesetzt wird.
    /// Nach Abschluss des Experiments werden die Testdaten kommasepariert im Ordner Experimente abgespeichert (Experimente\exp_yyMMdd_HH-mm-ss.csv).
    /// </para>
    /// </remarks>
    class Experiment2 : Experiment
    {
        /// <summary>
        /// IDs der doppelten Zustände.
        /// Wird zur Auswertung der Testdaten einmal in LadeTestUmgebungen und
        /// bei der csv-Ausgabe am Ende von StarteExperiment benötigt.
        /// </summary>
        int[] _arrIDsDoppelterZstd;
        /// <summary>
        /// Solwerte Q(s,a) der doppelten Zustände.
        /// Wird zur Auswertung der Testdaten einmal in LadeTestUmgebungen und
        /// bei der csv-Ausgabe am Ende von StarteExperiment benötigt.
        /// </summary>
        double[][] _sollwerte;

        Karte _umwelt2;
        /// <summary>
        /// Startkoordinaten für Kombination aus Karte1 (erstes Array) und Karte2 (zweites Array).
        /// </summary>
        protected static readonly HexKoordinate[][] _StartKoordinaten2 = 
        {
            new HexKoordinate[]{ // map9x10-01, Kosten=12
                //(-2,6), (0,4), (2,3), (5,1), (4,2), (6,1), (6,2), (5,4), (5,5)
                new HexKoordinate(6,1), new HexKoordinate(0,4), new HexKoordinate(2,3),
                /*new HexKoordinate(5,1),*/ new HexKoordinate(4,2), new HexKoordinate(-2,6),
                /*new HexKoordinate(6,2),*/ new HexKoordinate(5,4)/*, new HexKoordinate(5,5)*/
            },
            new HexKoordinate[]{ // map9x10-02, Kosten=12
                //(1,1), (0,2), (0,3), (-1,4), (-1,6), (6,1)
                new HexKoordinate(6,1), new HexKoordinate(1,1), new HexKoordinate(0,2),
                new HexKoordinate(0,3), new HexKoordinate(-1,4), new HexKoordinate(-1,6) }
        };


        public Experiment2() : base() {
        }


        /// <summary>
        /// Lädt zur spezfizerten Umwelt die Karte und Sollwerte und setzt den (oder einen neuen Agenten) in die Welt.
        /// Hinweis: Beendet automatisch eine Lernepisode durch Agent.Clear();
        /// </summary>
        /// <param name="karte">Typ der Umgebung</param>
        /// <returns>true, wenn Initialisierung erfolgreich; false sonst.</returns>
        public bool LadeTestUmgebungen(Karten karte1, Karten karte2) {
            int kartenNr1 = (int)karte1;
            var hexKarte1 = Karte.LoadMapFromFile(_MapFilenames[kartenNr1]);
            if (hexKarte1 == null) {
                return false;
            }
            int kartenNr2 = (int)karte2;
            var hexKarte2 = Karte.LoadMapFromFile(_MapFilenames[kartenNr2]);
            if (hexKarte2 == null) {
                return false;
            }
            Karte map1 = new Karte(hexKarte1);
            map1.Ziel = _ZielKordinaten[kartenNr1];
            Karte map2 = new Karte(hexKarte2);
            map2.Ziel = _ZielKordinaten[kartenNr2];

            // Zustand-IDs laden
            int[] zIds1 = LadeZIDs(_ZidFilenames[kartenNr1]);
            int[] zIds2 = LadeZIDs(_ZidFilenames[kartenNr2]);
            // Sollwerte laden
            double[][] sollwerte1 = LadeSollwerte(_SollFilenames[kartenNr1]);
            if (sollwerte1 == null)
                return false;
            if (sollwerte1.Length != zIds1.Length) {
                Debug.WriteLine("Inkonsistente Daten. Anzahl von ZustandsIDs und Sollwert-Arrays für erste Karte stimmen nicht überein.");
                return false;
            }
            double[][] sollwerte2 = LadeSollwerte(_SollFilenames[kartenNr2]);
            if (sollwerte2 == null)
                return false;
            if (sollwerte2.Length != zIds2.Length) {
                Debug.WriteLine("Inkonsistente Daten. Anzahl von ZustandsIDs und Sollwert-Arrays für zweite Karte stimmen nicht überein.");
                return false;
            }
            _maxStepsPerEpisode = 2 * zIds1.Length; // doppelte Zustandszahl als Schrittlimit pro Epsiode

            //Dictionary<int, double[]> testWerte = new Dictionary<int, double[]>();
            List<int> lIDsDoppelterZstd = new List<int>();
            List<double[]> sollList = new List<double[]>();
            for (int idx2 = 0; idx2 < zIds2.Length; idx2++) {
                int zID = zIds2[idx2];
                int duplicatIdx = Array.IndexOf<int>(zIds1, zID);
                if (duplicatIdx >= 0) {
                    // Duplikat! Sollwerte  für doppelte Zustände zusammenführen und Mittelwert bilden
                    double[] sollArr1 = sollwerte1[duplicatIdx];
                    double[] sollArr2 = sollwerte2[idx2];
                    double[] sollArrNew = new double[sollArr1.Length];
                    for (int k = 0; k < sollArr1.Length; k++) {
                        sollArrNew[k] = (sollArr1[k] + sollArr2[k]) / 2.0;
                    }
                    sollList.Add(sollArrNew);
                    lIDsDoppelterZstd.Add(zID);
                    //testWerte[zID] = sollArrNew;
                }
                else { /*nichts. nur doppelte Zustände betrachten!*/ }
            }
            _arrIDsDoppelterZstd = lIDsDoppelterZstd.ToArray(); // Array der den Welten gemeinsamen Zustands-IDs
            _sollwerte = sollList.ToArray(); // Sollwerte für die DOPPELTEN Zustände


            _umwelt = map1;
            _umwelt2 = map2;
            if (_agent == null) {
                _agent = new Agent(_umwelt);
            }
            else
                _agent.Umwelt = _umwelt;
            _agent.SetStates(_arrIDsDoppelterZstd); // Mit Menge gemeinsamer Zustände der beiden Welten
            _agent.SetSollwerte(_sollwerte);
            _agent.Reset(); 
            _startKoords = _StartKoordinaten2[kartenNr1];

            return true;
        }


        /// <summary>
        /// Startet ein Experiment und speichert die Ergebnisse als csv im Ordner "Experimente".
        /// </summary>
        /// <param name="episodes">optional: Anzahl Episoden je Durchlauf. Standard = 500.</param>
        /// <param name="bundleCount">optional: Anzahl Episoden, deren Ergebnisse jeweils gemittelt (gebündelt) werden. Standard = 10.<para>
        ///             episodes sollte ganzzahlig durch bundleCount teilbar sein!</para></param>
        /// <param name="passes">optional: Anzahl Durchläufe in diesem Experiment. Standard = 10. Bei jedem Durchlauf wird
        ///             das Agentenwissen gelöscht.</param>
        public override void StarteExperiment(int episodes = 500, int bundleCount = 10, int passes = 10) {
            Random random = new Random();
            int bundles = episodes / bundleCount;               // Anz. gemittelter Episoden-Bündel
            double[][] meanCostsPerPass = new double[passes][]; // je Testdurchlauf, durchschn. Kosten der Bündel (von je 10 Episoden)
            double[][] msePerPass = new double[passes][];       // je Testdurchlauf, mittl. quadr. Abweichung der Durchschn-Kosten von den echten Minimal-Kosten
            Matrix[][] qCommonsPerPass = new Matrix[passes][];

            // ------ Je Durchlauf... ------
            for (int pass = 1; pass <= passes; pass++) {
                _agent.Clear(); // Erfahrung vor jedem Testdurchlauf löschen!
                meanCostsPerPass[pass-1] = new double[bundles];
                msePerPass[pass-1] = new double[bundles];
                qCommonsPerPass[pass-1] = new Matrix[bundles];
                int epInBundle = 0;
                int stepCnt = 0;
                double kosten = 0.0;
                double error = 0.0;
                int bundle = 0;
                double[] kostenArr = new double[bundleCount];   // Gesamtkosten je Episode (innerhalb eines Bündels)
                double[] mseArr = new double[bundleCount];      // MSE je Episode (innerhalb eines Bündels)
                Matrix[] qCommonsArr = new Matrix[bundleCount]; // Q der gem. Zustände nach jeder Episode
                // Führe Lernepisoden aus:
                for (int episode = 1; episode <= episodes; episode++) {
                    // Karten-/Umweltwechsel nach jeder Episode
                    bool isKarte2 = episode % 2 == 0;
                    _agent.Umwelt = isKarte2 ? _umwelt2 : _umwelt;
                    base._startKoords = isKarte2 ? _StartKoordinaten2[1] : _StartKoordinaten2[0];

                    int startNr = random.Next(_startKoords.Length);
                    _agent.Umwelt.ReSpawnAgent(_startKoords[startNr]); // Zufallsauswahl für Startfeld
                    _agent.Reset(); // vor jeder Epis. Historie + Reward zurücksetzen
                    stepCnt = _agent.RunEpisode(_maxStepsPerEpisode);
                    kosten = _agent.RewardTotal;
                    kostenArr[epInBundle] = kosten;
                    error = _agent.GetMSE();  // Mittlere Quadratische Abweichung über alle gemeinsamen Q(s,a)
                    mseArr[epInBundle] = error;
                    qCommonsArr[epInBundle] = _agent.GetQVals(); // Q der gemeinsm. Zustände
                    epInBundle++;
                    // nach je 10 Epsidoden (einem Bündel), speichere Mittelwerte für reward, error und Q(s,a)
                    if (epInBundle == bundleCount) {
                        epInBundle = 0;
                        double meanCosts = kostenArr.Average();
                        double meanMSE = mseArr.Average();
                        meanCostsPerPass[pass-1][bundle] = meanCosts;
                        msePerPass[pass-1][bundle] = meanMSE;
                        // Mitteln der Qsa
                        qCommonsPerPass[pass-1][bundle] = qCommonsArr[0];
                        for (int i = 1; i < qCommonsArr.Length; i++) {
                            //qCommonsPerPass[pass-1] += qCommonsArr[i]; 
                            qCommonsPerPass[pass-1][bundle].Add(qCommonsArr[i]);
                        }
                        qCommonsPerPass[pass-1][bundle] /= qCommonsArr.Length;
                        bundle++;
                    }
                } // Ende des Durchlaufs
            } // Ende Experiment


            // Speichere Daten in csv
            StringBuilder sb = new StringBuilder();
            // --- HEADER ----
            sb.Append("Bundle;");
            for (int p = 0; p < passes; p++) {
                sb.AppendFormat("Costs(P{0});MSE(P{0});", (p+1));
                for (int s = 0; s < qCommonsPerPass[p][0].Rows; s++) {
                    for (int a = 0; a < qCommonsPerPass[p][0].Columns; a++) {
                        sb.AppendFormat("Q{0}(s{1},a{2});", (p+1), s, a);
                    }
                }
            }
            sb.Append("Costs;MSE;");
            for (int s = 0; s < qCommonsPerPass[0][0].Rows; s++) {
                for (int a = 0; a < qCommonsPerPass[0][0].Columns; a++) {
                    sb.AppendFormat("Q(s{0},a{1});", s, a);
                }
            }
            sb.AppendLine();

            // Bündel-Reihe für Reihe die Mittel-Werte ausgeben und
            // je Bündel am Ende noch ein Mittelwert über alle Durchläufe (Mittel der Mittelwerte) bilden und ausgeben.
            for (int bundle = 1; bundle <= bundles; bundle++) {
                sb.Append(bundle + ";");  // Bündelnummer

                // temp. Variablen zur Mittelung über alle Durchlüfe
                double costSum = .0; 
                double mseSum = .0;
                Matrix qAvg = new Matrix(qCommonsPerPass[0][bundle-1].Rows, qCommonsPerPass[0][bundle-1].Columns);
                
                // Über alle Durchläufe...
                for (int pass = 0; pass < passes; pass++) {
                    sb.Append(meanCostsPerPass[pass][bundle-1] + ";"); // Costs(Px)
                    sb.Append(msePerPass[pass][bundle-1] + ";"); // MSE(Px)
                    for (int s = 0; s < qAvg.Rows; s++) {
                        for (int a = 0; a < qAvg.Columns; a++) {
                            sb.Append(qCommonsPerPass[pass][bundle-1][s, a] + ";"); // Qx(s,a)
                        }
                    }

                    costSum += meanCostsPerPass[pass][bundle-1];
                    mseSum += msePerPass[pass][bundle-1];
                    qAvg += qCommonsPerPass[pass][bundle-1];
                }
                double costAvg = costSum / passes;
                double mseAvg = mseSum / passes;
                qAvg /= passes;
                
                sb.AppendFormat("{0};{1};", costAvg, mseAvg);
                for (int s = 0; s < qAvg.Rows; s++) {
                    for (int a = 0; a < qAvg.Columns; a++) {
                        sb.Append(qAvg[s, a] + ";");
                    }
                }
                sb.AppendLine();

            }
            // Soll-Mittelwerte
            sb.Append("Soll;");
            for (int pass = 0; pass <= passes; pass++) {
                sb.Append(";;");
                for (int s = 0; s < _sollwerte.Length; s++) {
                    for (int a = 0; a < _sollwerte[s].Length; a++) {
                        sb.Append(_sollwerte[s][a] + ";"); // Qx(s,a)
                    }
                }
            }
            
            // Lernparameter speichern?
            sb.AppendLine();
            sb.Append("Epsilon = " + _agent.Epsilon + "  ");
            sb.Append("Alpha = " + _agent.Alpha + "  ");
            sb.Append("Gamma = " + _agent.Gamma + "  ");
            sb.Append("Lambda = " + _agent.Lambda);
            sb.AppendLine();
            sb.Append("Episoden: " + episodes + "  ");
            sb.AppendLine("Ep. je Buendel: " + bundleCount);
            DateTime date = DateTime.Now;
            sb.AppendLine("Datum:" + date.ToShortDateString());

            // Zustand IDs speichern?
            for (int s = 0; s < _arrIDsDoppelterZstd.Length; s++) {
                sb.AppendFormat("#s{0}:;{1}", s, _arrIDsDoppelterZstd[s]);
                sb.AppendLine();
            }

            // Datei (Name?)
            if (!Directory.Exists("Experimente")) {
                Directory.CreateDirectory("Experimente");
            }
            File.WriteAllText(@"Experimente\exp_" + date.ToString("yyMMdd_HH-mm-ss") + ".csv", sb.ToString());
        }
    }
}
